﻿#region license
//Copyright 2008 Ritesh Rao 

//Licensed under the Apache License, Version 2.0 (the "License"); 
//you may not use this file except in compliance with the License. 
//You may obtain a copy of the License at 

//http://www.apache.org/licenses/LICENSE-2.0 

//Unless required by applicable law or agreed to in writing, software 
//distributed under the License is distributed on an "AS IS" BASIS, 
//WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 
//See the License for the specific language governing permissions and 
//limitations under the License. 
#endregion

using System;
using System.Collections.Generic;
using System.Data.Objects;
using System.Data.Objects.DataClasses;
using System.Linq;
using System.Linq.Expressions;
using NCommon.Expressions;

namespace NCommon.Data.EntityFramework
{
    /// <summary>
    /// Inherits from the <see cref="RepositoryBase{TEntity}"/> class to provide an implementation of a
    /// Repository that uses Entity Framework.
    /// </summary>
    public class EFRepository<TEntity> : RepositoryBase<TEntity>
    {
        #region fields
        private readonly ObjectContext _context;
        private ObjectQuery<TEntity> _contextQuery;
        private string _entitySetName;
        #endregion

        #region ctor
        /// <summary>
        /// Creates a new instance of the <see cref="EFRepository{TEntity}"/> class.
        /// </summary>
        public EFRepository()
        {
            Guard.Against<InvalidOperationException>(!UnitOfWork.HasStarted,
                                                     "No compatible UnitOfWork instance was found. Please start a compatible unit of work " +
                                                     "before creating the repository or use the constructor overload to explicitly provide a ObjectContext.");
            Guard.TypeOf<EFUnitOfWork>(UnitOfWork.Current,
                                              "The current UnitOfWork instance is not compatible with a EFRepository intance. " +
                                              "The EFRepository can only be used with EFUnitOfWork instances.");
            _context = ((EFUnitOfWork)UnitOfWork.Current).Context;
            SetContextQuery();
        }

        /// <summary>
        /// Overloaded Constructor.
        /// Creates a new instance of the <see cref="EFRepository{TEntity}"/> class that uses
        /// the specified ObjectContext
        /// </summary>
        /// <param name="context">The <see cref="ObjectContext"/> instance that the repository should use.</param>
        public EFRepository(ObjectContext context)
        {
            Guard.Against<ArgumentNullException>(context == null, "Expected a non-null ObjectContext instance.");
            _context = context;
            SetContextQuery();
        }
        #endregion

        #region methods
        /// <summary>
        /// Sets the <see cref="ObjectQuery{T}"/> for the entity from the current repository instance.
        /// </summary>
        /// <returns></returns>
        private void SetContextQuery()
        {
            foreach (var property in _context.GetType().GetProperties())
            {
                if (property.PropertyType.IsGenericType &&
                    property.PropertyType == typeof(ObjectQuery<TEntity>))
                {
                    _contextQuery = (ObjectQuery<TEntity>)property.GetValue(_context, null);
                    _entitySetName = property.Name;
                    return;
                }
            }
            throw new InvalidOperationException("The repository could not find a ObjectQuery for entity type " +
                                                typeof (TEntity).FullName);
        }
        #endregion

        #region Overrides of RepositoryBase<TEntity>
        /// <summary>
        /// Gets the <see cref="IQueryable{TEntity}"/> used by the <see cref="RepositoryBase{TEntity}"/> 
        /// to execute Linq queries.
        /// </summary>
        /// <value>A <see cref="IQueryable{TEntity}"/> instance.</value>
        protected override IQueryable<TEntity> RepositoryQuery
        {
            get { return _contextQuery; }
        }

        /// <summary>
        /// Marks the entity instance to be saved to the store.
        /// </summary>
        /// <param name="entity">An instance of <typeparamref name="TEntity"/> that should be saved
        /// to the database.</param>
        public override void Add(TEntity entity)
        {
            _context.AddObject(_entitySetName, entity);
        }

        /// <summary>
        /// Marks the changes of an existing entity to be saved to the store.
        /// </summary>
        /// <param name="entity">An instance of <typeparamref name="TEntity"/> that should be
        /// updated in the database.</param>
        /// <remarks>Implementors of this method must handle the Update scneario. </remarks>
        public override void Save(TEntity entity)
        {
            //Do nothing as Entity Framework uses change tracking to track changes.
        }

        /// <summary>
        /// Marks the entity instance to be deleted from the store.
        /// </summary>
        /// <param name="entity">An instance of <typeparamref name="TEntity"/> that should be deleted.</param>
        public override void Delete(TEntity entity)
        {
            _context.DeleteObject(entity);
        }

        /// <summary>
        /// Detaches a instance from the repository.
        /// </summary>
        /// <param name="entity">The entity instance, currently being tracked via the repository, to detach.</param>
        /// <exception cref="NotImplementedException">Implentors should throw the NotImplementedException if Detaching
        /// entities is not supported.</exception>
        public override void Detach(TEntity entity)
        {
            _context.Detach(entity);
        }

        /// <summary>
        /// Attaches a detached entity, previously detached via the <see cref="IRepository{TEntity}.Detach"/> method.
        /// </summary>
        /// <param name="entity">The entity instance to attach back to the repository.</param>
        /// <exception cref="NotImplementedException">Implentors should throw the NotImplementedException if Attaching
        /// entities is not supported.</exception>
        public override void Attach(TEntity entity)
        {
            _context.Attach(entity as IEntityWithKey);
        }

        /// <summary>
        /// Refreshes a entity instance.
        /// </summary>
        /// <param name="entity">The entity to refresh.</param>
        /// <exception cref="NotImplementedException">Implementors should throw the NotImplementedException if Refreshing
        /// entities is not supported.</exception>
        public override void Refresh(TEntity entity)
        {
            _context.Refresh(RefreshMode.StoreWins, entity);
        }

        /// <summary>
        /// Instructs the repository to eager load a child entities. 
        /// </summary>
        /// <param name="path">The path of the child entities to eager load.</param>
        /// <remarks>Implementors should throw a <see cref="NotSupportedException"/> if the underling provider
        /// does not support eager loading of entities</remarks>
        public override void With(Expression<Func<TEntity, object>> path)
        {
           With<TEntity>(path);
        }

        /// <summary>
        /// Instructs the repository to eager load entities that may be in the repository's association path.
        /// </summary>
        /// <param name="path">The path of the child entities to eager load.</param>
        /// <remarks>Implementors should throw a <see cref="NotSupportedException"/> if the underling provider
        /// does not support eager loading of entities</remarks>
        public override void With<T>(Expression<Func<T, object>> path)
        {
            Guard.Against<ArgumentNullException>(path == null, "Expected a non-null valid path expression.");
            var visitor = new MemberAccessPathVisitor();
            visitor.Visit(path);
            if (typeof(T) == typeof(TEntity))
                _contextQuery = _contextQuery.Include(visitor.Path);
            else
            {
                //The path represents an collection association. Find the property on the target type that
                //matches a IEnumerable<T> property.
                var pathExpression = visitor.Path;
                var targetType = typeof(TEntity);
                var matchesType = typeof(IEnumerable<T>);
                var targetProperty = (from property in targetType.GetProperties()
                                      where matchesType.IsAssignableFrom(property.PropertyType)
                                      select property).FirstOrDefault();
                if (targetProperty != null)
                    pathExpression = string.Format("{0}.{1}", targetProperty.Name, pathExpression);
                _contextQuery = _contextQuery.Include(pathExpression);
            }
        }
        #endregion
    }
}
